package za.org.jcicada.syshealth;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.DefaultComboBoxModel;
import javax.swing.MutableComboBoxModel;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.cfgmngr.IConfigurationClient;
import za.org.jcicada.cfgmngr.IConfigurationServer;
import za.org.jcicada.cfgmngr.IPhysicalClient;
import za.org.jcicada.cfgmngr.IPhysicalServer;
import za.org.jcicada.cfgmngr.ISystemObserver;
import za.org.jcicada.cfgmngr.ISystemSubject;
import za.org.jcicada.cfgmngr.SystemSetType;
import za.org.jcicada.cfgmngr.impl.CfgMngr;
import za.org.jcicada.cfgmngr.impl.ConfigurationServerProxy;
import za.org.jcicada.cfgmngr.impl.DefaultConfigurationClient;
import za.org.jcicada.cfgmngr.impl.DefaultPhysicalClient;
import za.org.jcicada.cfgmngr.impl.DefaultSystemObserver;
import za.org.jcicada.cfgmngr.impl.PhysicalServer;
import za.org.jcicada.cfgmngr.impl.PhysicalServerProxy;
import za.org.jcicada.cfgmngr.impl.SystemSubjectProxy;
import za.org.jcicada.datastore.IDataStoreServer;
import za.org.jcicada.datastore.impl.DataStoreServerProxy;
import za.org.jcicada.datastore.impl.MemoryDataStoreServer;
import za.org.jcicada.genss.PhysicalURN;
import za.org.jcicada.genss.ServiceURN;
import za.org.jcicada.genss.SystemURN;
import za.org.jcicada.genss.lifecycle.ILifeCycleClient;
import za.org.jcicada.genss.lifecycle.ILifeCycleServer;
import za.org.jcicada.genss.lifecycle.impl.DefaultLifeCycleClient;
import za.org.jcicada.genss.lifecycle.impl.LifeCycleServerProxy;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.ISamplingStrategy;
import za.org.jcicada.genss.sensor.ISensor;
import za.org.jcicada.genss.sensor.ISensorObserver;
import za.org.jcicada.genss.sensor.ISensorSubject;
import za.org.jcicada.genss.sensor.ISensorValue;
import za.org.jcicada.genss.sensor.SensorURN;
import za.org.jcicada.genss.sensor.impl.DefaultSensorObserver;
import za.org.jcicada.genss.sensor.impl.InvalidSensorValue;
import za.org.jcicada.genss.sensor.impl.Sensor;
import za.org.jcicada.genss.sensor.impl.SensorStatusValue;
import za.org.jcicada.genss.sensor.impl.StrategyPeriodicEvent;


/**
 * The Model component of the System Health UI Model-View-Controller implementation.
 * 
 * @author Robert Crida
 */
public class Model extends Observable {

	/** Logger object used for logging messages. */
	private static final Log logger = LogFactory.getLog(Model.class);

	public IConfigurationServer cfgServer;
	private IDataStoreServer dataStoreServer = new MemoryDataStoreServer();
	private IConfigurationClient cfgClient = new ConfigurationClient();
	private ISensorObserver sensorObserver = new SensorObserver();
	private ILifeCycleClient lifeCycleClient = new LifeCycleClient();
	private ISystemObserver systemObserver = new SystemObserver();
	private IPhysicalClient physicalClient = new PhysicalClient();
	private URI cfgMngrURI;
	private ISystemSubject systemSubject;
	private IPhysicalServer physicalServer;
	private SystemModel systemModel = new SystemModel();
	private SystemURN activeSystemURN;
	private ServiceURN cfgMngrServiceURN;
	private ServiceURN cfgMngrPhysicalURN;
	private SensorModel sensorModel;
	private ServiceModel serviceModel = new ServiceModel();
	private Map<SensorURN, Sensor> sensorMap;
	private Map<SensorURN, ISensorSubject> sensorSubjects = new HashMap<SensorURN, ISensorSubject>();
	private MutableComboBoxModel viewModel = new DefaultComboBoxModel();
	private PhysicalModel physicalModel = new PhysicalModel(this);
	private Map<SystemURN, ILifeCycleServer> lifeCycleServers = new HashMap<SystemURN, ILifeCycleServer>();
	private Map<ServiceURN, URI> serviceLocationURIs = new HashMap<ServiceURN, URI>();
	private Timer timer = new Timer();;
	private TimerTask connectTimerTask;

	/**
	 * Constructor creates a registration timer.
	 */
	public Model(URI cfgMngrURI) {
		this.cfgMngrURI = cfgMngrURI;
		try {
			cfgMngrServiceURN = new ServiceURN(new SystemURN(CfgMngr.SYSTEM_NAME), "systems");
			cfgMngrPhysicalURN = new ServiceURN(new SystemURN(CfgMngr.SYSTEM_NAME), PhysicalServer.URN_SERVICE);
		} catch (URISyntaxException ex) {
			logger.fatal("Failed to construct systems service URN given " + CfgMngr.SYSTEM_NAME);
			System.exit(1);
		}
		initialize();
		connect();
	}

	private void initialize() {
		if (systemSubject != null) {
			systemSubject.informDisconnect(systemObserver);
		}
		if (physicalServer != null) {
			physicalServer.informDisconnect(physicalClient);
		}
		systemModel.clear();
		activeSystemURN = null;
		if (sensorModel != null) {
			sensorModel.clear();
		}
		if (sensorMap != null) {
			sensorMap.clear();
			sensorMap = null;
		}
		for (ISensorSubject sensorSubject : sensorSubjects.values()) {
			sensorSubject.informDisconnect(sensorObserver);
		}
		sensorSubjects.clear();
		SensorURISubjectCache.getInstance().clear();
		for (ILifeCycleServer lifeCycleServer : lifeCycleServers.values()) {
			lifeCycleServer.informDisconnect(lifeCycleClient);
		}
		lifeCycleServers.clear();
	}

	private void connect() {
		if (connectTimerTask == null) {
			cfgServer = new ConfigurationServerProxy(cfgMngrURI);
			physicalModel.initialize(cfgServer);
			timer.scheduleAtFixedRate(connectTimerTask = new TimerTask() {

				@Override
				public void run() {
					cfgServer.requestLocateService(cfgClient, cfgMngrServiceURN, "tcp");
					cfgServer.requestLocateService(cfgClient, cfgMngrPhysicalURN, "tcp");
				}

			}, 0L, 1000L);
		}
	}

	/*
	 * View methods
	 */

	public MutableComboBoxModel getViewModel() {
		return viewModel;
	}
	
	public PhysicalModel getPhysicalModel() {
		return physicalModel;
	}
	
	public SystemModel getSystemModel() {
		return systemModel;
	}

	public ServiceModel getServiceModel() {
		return serviceModel;
	}

	public String getServiceLocation(ServiceURN serviceURN) {
		URI locationURI = serviceLocationURIs.get(serviceURN);
		return locationURI != null ? locationURI.toString() : "Unknown";
	}

	public SensorModel getSensorModel(SystemURN systemURN) {
		// detach from sensors for the current system
		if (sensorModel != null) {
			for (int i = 0; i < sensorModel.getSize(); i++) {
				ISensor sensor = (ISensor) sensorModel.getElementAt(i);
				try {
					SensorURN sensorURN = new SensorURN(activeSystemURN, sensor.getName());
					ISensorSubject sensorSubject = sensorSubjects.get(sensorURN);
					if (sensorSubject != null) {
						Set<SensorURN> sensorURNs = new HashSet<SensorURN>();
						sensorURNs.add(sensorURN);
						sensorSubject.requestDetach(sensorObserver, sensorURNs);
					}
				} catch (URISyntaxException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
			sensorModel.clear();
		}
		sensorModel = new SensorModel();
		sensorMap = new HashMap<SensorURN, Sensor>();
		activeSystemURN = systemURN;
		try {
			if (systemURN != null) {
				cfgServer.requestSensorSet(cfgClient, systemURN);
				cfgServer.requestServiceSet(cfgClient, systemURN);
				cfgServer.requestLocateService(cfgClient, new ServiceURN(systemURN, "lifecycle"), "tcp");
			}
		} catch (URISyntaxException ex) {
			ex.printStackTrace();
		}
		return sensorModel;
	}

	public Sensor getSensor(SensorURN sensorURN) {
		return sensorMap.get(sensorURN);
	}
	
	public IDataStoreServer getDataStoreServer() {
		return dataStoreServer;
	}
	
	public IPhysicalServer getPhysicalServer() {
		return physicalServer;
	}

	/*
	 * Control methods
	 */

	public void halt(SystemURN systemURN) {
		lifeCycleServers.get(systemURN).requestHalt(lifeCycleClient, systemURN);
	}

	public void restart(SystemURN systemURN) {
		lifeCycleServers.get(systemURN).requestRestart(lifeCycleClient, systemURN);
	}

	public void selectPhysicalNode(PhysicalURN physicalURN, boolean isLeaf) {
		if (isLeaf) {
			physicalServer.requestGetChildren(physicalClient, physicalURN);
		}
		systemModel.clear();
		physicalServer.requestNodeSystems(physicalClient, physicalURN);
	}
	
	private class ConfigurationClient extends DefaultConfigurationClient {
		
		@Override
		public void informDisconnect() {
			logger.warn("Disconnected from ConfigurationServer");
			initialize();
			connect();
		}

		@Override
		public void replyLocateService(Result result, ServiceURN serviceURN, URI uri) {
			logger.info(String.format("Located %s at %s", serviceURN, uri));
			serviceLocationURIs.put(serviceURN, uri);
			if (result.getCode() == SuccessCode.COMPLETE_SUCCESS) {
				if (uri.getScheme().equals("tcp")) {
					if (connectTimerTask != null) {
						connectTimerTask.cancel();
						connectTimerTask = null;
					}
					if (serviceURN.getInterface().equals("lifecycle")) {
						ILifeCycleServer lifeCycleServer = new LifeCycleServerProxy(uri);
						lifeCycleServers.put(serviceURN.getSystemURN(), lifeCycleServer);
					}
					if (serviceURN.getInterface().equals("systems")) {
						systemSubject = new SystemSubjectProxy(uri);
						systemSubject.requestAttach(systemObserver);
					}
					if (serviceURN.getInterface().equals(PhysicalServer.URN_SERVICE)) {
						physicalServer = new PhysicalServerProxy(uri);
						physicalServer.requestAttach(physicalClient);
					}
					if (serviceURN.getInterface().equals("datastore")) {
						logger.info(String.format("DataStore at %s", uri));
						dataStoreServer = new DataStoreServerProxy(uri);
					}
					if (serviceURN.getService().equals(SensorURN.URN_SERVICE_SENSORS)) {
						ISensorSubject sensorSubject = SensorURISubjectCache.getInstance().getSensorSubject(uri);
						SensorURN sensorURN = (SensorURN) serviceURN;
						sensorSubjects.put(sensorURN, sensorSubject);
						Set<SensorURN> sensorURNs = new HashSet<SensorURN>();
						sensorURNs.add(sensorURN);
						sensorSubject.requestAttach(sensorObserver, sensorURNs);
					}
				}
			} else {
				logger.warn(result.getMessage());
			}
		}

		@Override
		public void replySystemSet(Result arg0, SystemSetType type, Set<SystemURN> arg1) {
			systemModel.setElements(arg1);
		}
		
		@Override
		public void replySensorSet(Result result, SystemURN systemURN, Set<SensorURN> sensorURNs) {
			if (result.getCode() == SuccessCode.COMPLETE_SUCCESS) {
				if (activeSystemURN.equals(systemURN)) {
					for (SensorURN sensorURN : sensorURNs) {
						//sensorModel.addElement(sensorURN);
						cfgServer.requestLocateService(this, sensorURN, "tcp");
					}
				}
			} else {
				logger.error(result.toString());
			}
		}
		
		@Override
		public void replyServiceSet(Result result, SystemURN systemURN, Set<ServiceURN> serviceURNs) {
			if (activeSystemURN.equals(systemURN)) {
				serviceModel.setElements(serviceURNs);
				for (ServiceURN serviceURN : serviceURNs) {
					cfgServer.requestLocateService(this, serviceURN, "katcp");
				}
			}
		}

	}

	private class SensorObserver extends DefaultSensorObserver {
		
		@Override
		public void informSensor(SensorURN sensorURN, ISensorValue value) {
			//logger.info(String.format("Received %s for %s", value.getValue(), sensorURN));
			try {
				Sensor sensor = sensorMap.get(sensorURN);
				if (sensor != null) {
					sensor.setValue(value);
				}
			} catch (InvalidSensorValue ex) {
				logger.error(String.format("Failed to set value for sensor %s", sensorURN), ex);
			}
		}
		
		@Override
		public void informSensorStatus(SensorURN sensorURN, SensorStatusValue statusValue) {
			Sensor sensor = sensorMap.get(sensorURN);
			if (sensor != null) {
				sensor.setStatusValue(statusValue);
			}
		}
	
		@Override
		public void informSensorType(SensorURN sensorURN, ISensor sensor) {
			logger.info(String.format("Sensor %s type %s", sensorURN, sensor));
			if (activeSystemURN.equals(sensorURN.getSystemURN())) {
				if (!sensorMap.containsKey(sensorURN)) {
					sensorMap.put(sensorURN, (Sensor) sensor);
					sensorModel.addElement(sensor);
				}
			}
		}

		@Override
		public void replyAttach(Result result, Set<SensorURN> sensorURNs) {
			for (SensorURN sensorURN: sensorURNs) {
				ISamplingStrategy strategy = new StrategyPeriodicEvent(sensorURN, 5000);
				sensorSubjects.get(sensorURN).requestSetSamplingStrategy(this, strategy);
			}
		}
	
	}
	
	private class LifeCycleClient extends DefaultLifeCycleClient {
	}
	
	private class SystemObserver extends DefaultSystemObserver {
		
		@Override
		public void informSystemEvent(final SystemURN systemURN, SystemSetType type, Event event) {
			logger.info(String.format("%s %s %s", systemURN, type, event));
			if (type == SystemSetType.VIRTUAL) {
				if (event == ISystemObserver.Event.REGISTERED) {
					systemModel.addElement(systemURN);
				} else {
					systemModel.removeElement(systemURN);
				}
			}
			if (systemURN.isDataStore()) {
				if (type == SystemSetType.REGISTERED) {
					if (event == ISystemObserver.Event.REGISTERED) {
						timer.schedule(new TimerTask() {

							@Override
							public void run() {
								try {
									cfgServer.requestLocateService(cfgClient, new ServiceURN(systemURN, "datastore"), "tcp");
								} catch (URISyntaxException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
							}
							
						}, 1000);
					}
				}
			}
		}

	}
	
	private class PhysicalClient extends DefaultPhysicalClient {
		
		@Override
		public void replyGetChildren(Result result, PhysicalURN parentURN, List<PhysicalURN> childrenURNs) {
			physicalModel.setChildrenOf(parentURN, childrenURNs);
		}

		@Override
		public void replyNodeSystems(Result result, PhysicalURN physicalURN, Set<SystemURN> systemURNs) {
			if (result.getCode() == SuccessCode.COMPLETE_SUCCESS) {
				systemModel.setElements(systemURNs);
			} else {
				logger.error(result.toString());
			}
		}
		
		@Override
		public void informViewEvent(String view, IPhysicalClient.Event event) {
			if (event == Event.ADDED) {
				viewModel.addElement(view);
			} else {
				viewModel.removeElement(view);
			}
		}

	}

}
